<?php
use frontend\modules\yii2\widgets\layouts\markdown\Markdown;

$this->title = 'RESTful API的学习和使用';
$this->registerMetaTag([
    'name' => 'keywords',
    'content' => 'yii,yii2,框架,应用结构,web,controller,api,RESTful,请求,接口,控制器,使用,教程'
]);
$this->registerMetaTag([
    'name' => 'description',
    'content' => 'RESTful Api是一种比较成熟的接口设计理论，它使得前后端的开发有了完善的数据交互规范。'
]);
$this->registerMetaTag([
    'name' => 'author',
    'content' => 'chenzhiwei'
]);


$html = [
    "overall" => <<<HTML
# RESTful API的学习和使用
我们在开发服务端应用的时候，在很多场景下要与前端开发人员合作，服务端提供数据，前端展示数据，此时就需要服务端和前端配合协商接口，为了减少沟通内容，规范接口风格，RESTful API应运而生。
HTML,
    "概述" => <<<HTML
## 概述 id=overview
RESTful API对接口的如下几方面进行了规范：
- **把数据抽象为资源进行操作**  
在Yii中，可以把模型（Model）看做是资源，而RESTful API则是对模型类的操作（增删改查）；

- **统一资源入口（URL）**  
我们不需要为资源的各种操作（增删改查）都设置一个URL，而只要向外发布一个URL即可，如：
```shell
http://localhost/api/user
```
但在实现API的控制器中，你需要实现增删改查方法。
- **不同的HTTP方法（GET/POST/DELETE/HEAD/OPTIONS）抽象为对资源的不同操作**  
因为RESTful API只提供了一个资源入口，因此通过不同的HTTP方法来确定进入不同的控制器动作，如：GET方法获取资源，POST/PUT方法用于创建和修改资源，DELETE方法用于删除资源。请查看[路由机制](#route)了解如何解析HTTP方法到对应的控制器动作。

HTML,
    "父类" => <<<HTML
## 父类 id=parent
通常我们会对一张数据表进行增删改查，模型类也是以表为单位进行设计的，因此Yii在引入RESTful API时专门设计了一个控制器类（[ActiveController](https://www.yiichina.com/doc/api/2.0/yii-rest-activecontroller)）。它以某个模型类为操作资源，封装了独立的增删改查操作，简化了API的开发流程。
```php
namespace frontend\modules\api\controllers;

use yii\\rest\ActiveController;

/**
 * Default controller for the `api` module
 */
class UserController extends ActiveController
{
    public \$modelClass = "common\models\User";
}
```
> 注意：使用[ActiveController](https://www.yiichina.com/doc/api/2.0/yii-rest-activecontroller)时，必须设置`\$modelClass`，否则会报错。

我们先来看一下[ActiveController](https://www.yiichina.com/doc/api/2.0/yii-rest-activecontroller)的代码：
```php
namespace yii\\rest;

use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\web\ForbiddenHttpException;

class ActiveController extends Controller
{

    public \$modelClass;

    // 指定更新操作的场景
    public \$updateScenario = Model::SCENARIO_DEFAULT;
    // 指定创建操作的场景
    public \$createScenario = Model::SCENARIO_DEFAULT;

    public function init()
    {
        parent::init();
        if (\$this->modelClass === null) {
            throw new InvalidConfigException('The "modelClass" property must be set.');
        }
    }

    // 使用外部独立动作，提高代码复用率
    public function actions()
    {
        return [
            'index' => [
                'class' => 'yii\\rest\IndexAction',
                'modelClass' => \$this->modelClass,
                'checkAccess' => [\$this, 'checkAccess'],
            ],
            'view' => [
                'class' => 'yii\\rest\ViewAction',
                'modelClass' => \$this->modelClass,
                'checkAccess' => [\$this, 'checkAccess'],
            ],
            'create' => [
                'class' => 'yii\\rest\CreateAction',
                'modelClass' => \$this->modelClass,
                'checkAccess' => [\$this, 'checkAccess'],
                'scenario' => \$this->createScenario,
            ],
            'update' => [
                'class' => 'yii\\rest\UpdateAction',
                'modelClass' => \$this->modelClass,
                'checkAccess' => [\$this, 'checkAccess'],
                'scenario' => \$this->updateScenario,
            ],
            'delete' => [
                'class' => 'yii\\rest\DeleteAction',
                'modelClass' => \$this->modelClass,
                'checkAccess' => [\$this, 'checkAccess'],
            ],
            'options' => [
                'class' => 'yii\\rest\OptionsAction',
            ],
        ];
    }

    // 方法过滤
    protected function verbs()
    {
        return [
            'index' => ['GET', 'HEAD'],
            'view' => ['GET', 'HEAD'],
            'create' => ['POST'],
            'update' => ['PUT', 'PATCH'],
            'delete' => ['DELETE'],
        ];
    }

    // 权限控制
    public function checkAccess(\$action, \$model = null, \$params = [])
    {
    }
}
```
HTML,
    "独立动作" => <<<HTML
### 独立动作
我们之前介绍过独立动作（[前往查看](/docs/structure/controller#extra-action)），而Yii为[ActiveController](https://www.yiichina.com/doc/api/2.0/yii-rest-activecontroller)封装了6个独立动作（定义在控制器之外的公共动作）实现增删改查功能：
- `index`：以分页的形式查询资源集合；
- `view`：获取资源的某条记录；
- `create`：新增一条新的记录；
- `update`：更新资源；
- `delete`：删除资源；
- `options`：查询资源的可用方法（POST/GET/PUT...）；

HTML,
    "route" => <<<HTML
#### 路由机制 id=route
Yii为RESTful API封装了专用的路由机制，实现了通过HTTP方法（POST/GET/DELETE...）和传入的参数就可以确定目标控制器动作的功能。你只需要对`urlManager`组件作如下配置即可：
```php
'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'rules' => [
        ['class' => 'yii\\rest\UrlRule', 'controller' => 'api/user'],
    ],
],
```
通过阅读`yii\\rest\UrlRule`这个类可知，`urlManager`组件会根据上述的配置自动创建如下几个路由：
- `'PUT,PATCH users/<id>' => 'user/update'`: update a user
- `'DELETE users/<id>' => 'user/delete'`: delete a user
- `'GET,HEAD users/<id>' => 'user/view'`: return the details/overview/options of a user
- `'POST users' => 'user/create'`: create a new user
- `'GET,HEAD users' => 'user/index'`: return a list/overview/options of users
- `'users/<id>' => 'user/options'`: process all unhandled verbs of a user
- `'users' => 'user/options'`: process all unhandled verbs of user collection

> 注意：我们定义的是`UserController`，但是经过`yii\\rest\UrlRule`解析后会默认在url中以复数的形式展现控制器id：`users`。如果想取消这个功能，可以作如下配置：
```php
'urlManager' => [
    'enablePrettyUrl' => true,
    'showScriptName' => false,
    'rules' => [
        [
            'class' => 'yii\\rest\UrlRule', 
            'controller' => 'api/user', 
            'pluralize' => false // 取消复数形式
        ],
    ],
],
```

经过URL配置后，我们就成功创建了一组RESTful风格的API，这些API包括：
- `GET /users`: 逐页列出所有用户
- `HEAD /users`: 显示用户列表的概要信息
```php
curl -i --head "http://localhost/api/users"
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Wed, 22 Dec 2021 09:10:58 GMT
Content-Type: application/json; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/8.0.7
Vary: Accept
X-Pagination-Total-Count: 29
X-Pagination-Page-Count: 2
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://localhost/api/users?page=1>; rel=self, 
        <http://localhost/api/users?page=1>; rel=first, 
        <http://localhost/api/users?page=2>; rel=last, 
        <http://localhost/api/users?page=2>; rel=next
```
- `POST /users`: 创建一个新用户
- `GET /users/123`: 返回用户 123 的详细信息
- `HEAD /users/123`: 显示用户 123 的概述信息
- `PATCH /users/123` and `PUT /users/123`: 更新用户123
- `DELETE /users/123`: 删除用户123
- `OPTIONS /users`: 显示关于末端 `/users` 支持的动词
- `OPTIONS /users/123`: 显示有关末端 `/users/123` 支持的动词
HTML,
    "index" => <<<HTML
#### indexAction id=index
这个独立动作提供以分页的方式返回资源列表的功能，常用的场景就是手机APP中下拉刷新的功能。它在`ActiveController`中的默认配置如下：
```php
public function actions()
{
    return [
        'index' => [
            'class' => 'yii\\rest\IndexAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
        ],
        ... ...
    ];
}
```
这样的配置已经可以正常工作了，但是如果你仔细阅读`yii\\rest\IndexAction`源码的话，会发现还有很多实用的配置可以使用：  
- **prepareDataProvider：数据提供者，定义分页的细节**    
`yii\\rest\IndexAction`已经实现了一个默认的`prepareDataProvider()`方法，但是允许我们给属性`\$prepareDataProvider`赋值一个匿名函数来覆盖同名方法，使用举例如下：
```php
public function actions()
{
    return [
        'index' => [
            'class' => 'yii\\rest\IndexAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
            'prepareDataProvider' => function (IndexAction \$action) {
                // \$action is the action object currently running
                // do your jobs
                
                // The callable should return an instance of [[ActiveDataProvider]].
                return Yii::createObject([
                    'class' => ActiveDataProvider::className(),
                    'query' => \$action->modelClass::find()->where(["<", "id", "1000"])
                ]);
            },
//          如果您定义了dataFilter属性，则可以把查询条件传入第二个参数filter            
//          'prepareDataProvider' => function (IndexAction \$action, mixed \$filter) {
//              return Yii::createObject([
//                  'class' => ActiveDataProvider::className(),
//                  'query' => \$action->modelClass::find()->andWhere(\$filter)
//              ]);
//          }
        ],
        ... ...
    ];
}
```
上面的例子中对结果集进行了过滤，只向客户端展示用户ID在1000内的用户。
> 正是因为可以重写`prepareDataProvider`数据提供者的`query`查询语句，才打破了RESTful API在默认情况下只能对单表（`modelClass`指定的表）进行操作的限制，你可以在`query`中写多表联合查询，以满足更复杂的查询需求。

- **prepareSearchQuery：where条件查询**  
这个属性也接收一个匿名函数，功能是给默认的`prepareDataProvider()`数据提供者增加自定义的查询条件，因此如果你对我们刚刚介绍的`prepareDataProvider`属性赋了值，则会让本属性的设置失效。使用举例：
```php
public function actions()
{
    return [
        'index' => [
            'class' => 'yii\\rest\IndexAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
            'prepareSearchQuery' => function (\$query, \$requestParams) {
                // set your filters : 
                // \$query->andFilterWhere(['id' => 1]);
                
                return \$query;
            }
        ],
        ... ...
    ];
}
```

- **dataFilter：来自客户端的数据查询条件**  
你没看错，`yii\\rest\IndexAction`还可以让客户端来定义sql语句的查询条件，这在一定程度上减少了接口数量。在一般的接口设计中，客户端只能传入字段的值来请求数据，但是`dataFilter`却可以支持让客户定义完整的`where`语句，并对查询条件中的数据进行校验，举例如下：
```php
public function actions()
{
    return [
        'index' => [
            'class' => 'yii\\rest\IndexAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
            'dataFilter' => [
                'class' => 'yii\data\ActiveDataFilter',
                'searchModel' => function () {
                    return (new \common\models\User());
                },
            ]
        ],
        ... ...
    ];
}
```
有了上述`dataFilter`的定义以后，我们就可以对`\common\models\User`类中`\$scenario='default'`的属性进行条件查询（如果你不了解`scenario`，请前往[scenario章节](/docs/structure/model#scenario)）：
```php
// 查询User表中已被删除的用户（status=0）
curl -i 'http://localhost/api/users?filter[status]=0'
```
如果查询条件简单，用上例`filter`数组传值即可，但是如果查询条件很复杂，那用数组传值就不乐观了：
```php
"filter": {
    "or": [
        {
            "and": [
                {
                    "name": "some name",
                },
                {
                    "price": "25",
                }
            ]
        },
        {
            "id": {"in": [2, 5, 9]},
            "price": {
                "gt": 10,
                "lt": 50
            }
        }
    ]
}
```
上述条件很复杂，但是用json格式表述却很清晰，因此可以将json字符串作为filter的值。对于`GET`方法的传值，Yii并没有检测json格式并自动转换为数组的功能，因此需要我们自己写：
```php
public function actions()
{
    // 转换filter条件json格式到数组
    \$params = Yii::\$app->request->get();
    if(isset(\$params["filter"]) && is_string(\$params["filter"])) {
        \$params["filter"] = json_decode(\$params["filter"], 1) ?? \$params["filter"];
        Yii::\$app->request->setQueryParams(\$params);
    }
    return [
        'index' => [
            'class' => 'yii\\rest\IndexAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
            'dataFilter' => [
                'class' => 'yii\data\ActiveDataFilter',
                'searchModel' => function () {
                    return (new \yii\base\DynamicModel(['id' => null, 'name' => null, 'price' => null]))
                        ->addRule('id', 'integer')
                        ->addRule('name', 'trim')
                        ->addRule('name', 'string')
                        ->addRule('price', 'number');
                },
            ]
        ],
        ... ...
    ];
}
```
由于参数是json字符串，请在请求url前对参数进行url格式转换，如：
```php
// 请不要直接以如下方式请求数据
curl -i 'http://localhost/api/users?filter={"and":[{"id":1},{"username":"master"}]}'
// 请url编码后再请求
curl -i 'http://localhost/api/users?filter=%7B%22and%22%3A%5B%7B%22id%22%3A1%7D%2C%7B%22username%22%3A%22master%22%7D%5D%7D'
```

HTML,
    "view" => <<<HTML
#### viewAction id=view
此动作是查询单条记录的，您需要在url中传入一个值用于唯一确定一条记录，这个值一般是表的单值主键（id），也可以是联合主键，此时您应该传入多个值并用逗号（,）分开。默认的引用如下：
```php
public function actions(){
    return [
        'view' => [
            'class' => 'yii\\rest\ViewAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
        ],
    ];
}
```


使用主键id的情况：
```php
curl -i 'http://localhost/api/user/1'
```
使用联合索引的情况：
```php
// 假设PRIMARY KEY (`username`,`email`)
curl -i 'http://localhost/api/user/lvzmen,admin@lvzmen.com'
```

HTML,
    "create" => <<<HTML
#### createAction id=create
本独立动作用于创建新的记录，只能使用POST方法触发，URL路由如下：
```php
['POST users' => 'user/create']
```
使用方法举例如下：
```php
public function actions(){
    return [
        'create' => [
            'class' => 'yii\\rest\CreateAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
            'scenario' => \$this->createScenario,
        ],
    ];
}
```
大家可以看到使用这个动作需要指定场景（[scenario](/docs/structure/model#scenario)），默认值为`default`，这是为了精确控制块赋值的目标属性以及值的校验，避免对不相关的属性进行赋值和校验（很大可能会校验失败）导致创建记录失败。

HTML,
    "update" => <<<HTML
#### updateAction id=update
本动作用于更新数据，您只能用`PUT`和`PATCH`方法传递参数，默认路由如下：
```php
['PUT,PATCH users/<id>' => 'user/update']
```
路由中的`<id>`和 [viewAction](#view) 中的规则一致，是表中的主键值，用于唯一确定一条记录，可以是单值主键也可以是联合主键，在联合主键下需要将所有值用逗号（,）分开。

使用举例：
```php
public function actions(){
    return [
        'update' => [
            'class' => 'yii\\rest\UpdateAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
            'scenario' => \$this->updateScenario,
        ],
    ];
}
```
[scenario](/docs/structure/model#scenario)的含义和[createAction](#create)是一致的，您应该将允许被更新的属性独立定义为一个场景，以避免不允许被更新的属性受到影响（用户可能会在请求体中新增一些字段攻击系统）。

HTML,
    "delete" => <<<HTML
#### deleteAction id=delete
本动作用于删除资源，只允许通过`DELETE`方法发送请求，路由如下：
```php
['DELETE users/<id>' => 'user/delete']
```
路由中的`<id>`和 [viewAction](#view) 中的规则一致，是表中的主键值，用于唯一确定一条记录，可以是单值主键也可以是联合主键，在联合主键下需要将所有值用逗号（,）分开。
使用举例：
```php
public function actions(){
    return [
        'delete' => [
            'class' => 'yii\\rest\DeleteAction',
            'modelClass' => \$this->modelClass,
            'checkAccess' => [\$this, 'checkAccess'],
        ],
    ];
}
```
HTML,
    "options" => <<<HTML
#### optionsAction id=options
这个独立动作提供了客户端查询接口中可用访问方法的信息，路由如下：
```php
['users/<id>' => 'user/options'],
['users' => 'user/options']
```
如果客户端不清楚接口中都允许使用什么方法进行访问，可以尝试如下调用：
```php
curl -i --request OPTIONS 'http://localhost/api/users'
HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Fri, 24 Dec 2021 01:24:52 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/8.0.7
Vary: Accept
Allow: GET, POST, HEAD, OPTIONS
Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS
```
上述响应头中明确显示了当前接口只接受GET, POST, HEAD, OPTIONS这些方法，因此你不能发送修改和删除请求。

独立动作使用方法如下：
```php
public function actions(){
    return [
        'options' => [
            'class' => 'yii\\rest\OptionsAction',
        ],
    ];
}
```


HTML,
    "格式化响应" => <<<HTML
## 格式化响应 id=response-format
### Envelope包装 id=envelope
如果您的api控制器是继承`ActiveController`，那你在获取资源列表（访问`indexAction`动作）时，可以对返回值进行包装：
```php
use yii\\rest\ActiveController;

class UserController extends ActiveController
{
    public \$modelClass = 'app\models\User';
    public \$serializer = [
        'class' => 'yii\\rest\Serializer',
        'collectionEnvelope' => 'items',
    ];
}
```
返回值举例：
```php
curl -i 'http://localhost/api/user'
HTTP/1.1 200 OK
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
X-Powered-By: PHP/5.4.20
X-Pagination-Total-Count: 1000
X-Pagination-Page-Count: 50
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://localhost/api/users?page=1>; rel=self,
      <http://localhost/api/users?page=2>; rel=next,
      <http://localhost/api/users?page=50>; rel=last
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8

{
    "items": [
        {
            "id": 1,
            ...
        },
        {
            "id": 2,
            ...
        },
        ...
    ],
    "_links": {
        "self": {
            "href": "http://localhost/api/users?page=1"
        },
        "next": {
            "href": "http://localhost/api/users?page=2"
        },
        "last": {
            "href": "http://localhost/api/users?page=50"
        }
    },
    "_meta": {
        "totalCount": 1000,
        "pageCount": 50,
        "currentPage": 1,
        "perPage": 20
    }
}
```

HTML,
    "控制json输出" => <<<HTML
### 控制json输出 id=json-format
在我们开发接口的时候，经常会把数据以json格式返回给客户端，因此对输出的json字符串进行适当的控制会很有用，您可以在配置文件中定义控制内容：
```php
'response' => [
    // ...
    'formatters' => [
        \yii\web\Response::FORMAT_JSON => [
            'class' => 'yii\web\JsonResponseFormatter',
            'prettyPrint' => YII_DEBUG, // use "pretty" output in debug mode
            'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
            // ...
        ],
    ],
],
```
上述配置可以格式化json字符串，使它更容易被查看，举例如下：
```php
curl -i 'http://localhost/api/user'
HTTP/1.1 200 OK
Date: Fri, 24 Dec 2021 14:41:14 GMT
Server: Apache/2.4.38 (Debian)
Vary: Accept
Content-Length: 49
Content-Type: application/json; charset=UTF-8

{
    "message": {
        "name": "chen"
    }
}
```

HTML,
    "" => <<<HTML


HTML,

];
echo Markdown::widget(["content" => implode("\n", $html)]);


